package org.nativejpa;

import org.apache.log4j.Logger;

import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.sql.DataSource;
import java.util.Collections;
import java.util.Map;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;

import org.nativejpa.cache.CacheConfig;
import org.nativejpa.cache.CacheFactory;
import org.nativejpa.datasource.AtomikosNonXADataSourceFactory;
import org.nativejpa.datasource.AtomikosXADataSourceFactory;
import org.nativejpa.query.NamedSQLTemplate;
import org.nativejpa.transaction.TransactionManagerFactory;
import org.nativejpa.transaction.EntityTransactionImpl;
import org.nativejpa.datasource.DataSourceFactory;
import org.nativejpa.parser.PersistenceUnit;

/**
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 * @create 2009-3-17 16:20:40
 */
public class EntityManagerFactoryImpl implements EntityManagerFactory {

    private static Logger logger = Logger.getLogger(EntityManagerFactoryImpl.class);

    private String unitName;

    /**
     * jta-data-source in persistence.xml
     */
    private String jtaDataSource;

    private EntityManagerFactoryBuilder emFactoryBuilder = null;

    private DataSourceFactory dataSourceFactory = null;
    private DataSource dataSource = null;

    private boolean open = true;

    private CacheFactory cacheFactory = null;

    private TransactionManagerFactory transactionManagerFactory;

    private EntityTransaction entityTransaction = null;

    private String databaseProductName = null;

    private PersistenceUnit persistenceUnit;

    public EntityManagerFactoryImpl(PersistenceUnit persistenceUnit, EntityManagerFactoryBuilder emFactoryBuilder) {
        this.persistenceUnit = persistenceUnit;
        this.unitName = persistenceUnit.getUnitName();
        this.emFactoryBuilder = emFactoryBuilder;
        this.jtaDataSource = persistenceUnit.getJtaDataSourceName();
        
        if(persistenceUnit.getDataSourceXADriver() != null){
            this.dataSourceFactory = new AtomikosXADataSourceFactory();
        }
        else {
            this.dataSourceFactory = new AtomikosNonXADataSourceFactory();
        }

        this.transactionManagerFactory = emFactoryBuilder.getTransactionManagerFactory();
        CacheConfig cacheConfig = CacheConfig.newCacheConfig(unitName, persistenceUnit.getAllProperties());
        this.cacheFactory = CacheFactory.newCacheFactory(cacheConfig);
        initDataSource();
    }

    protected void initDataSource(){
        this.dataSource = dataSourceFactory.createDataSource(persistenceUnit, transactionManagerFactory.getTransactionManager());
        // get database type
        if (databaseProductName == null) {
            Connection conn = null;
            try {
                conn = dataSource.getConnection();
                DatabaseMetaData dbMeta = conn.getMetaData();
                databaseProductName = dbMeta.getDatabaseProductName();
            }
            catch (SQLException e) {
                logger.warn("Exception while getDatabaseType for DataSource " + dataSource, e);
                databaseProductName = "Unkown";
            }
            finally {
                try {
                    if (conn != null && !conn.isClosed()) {
                        conn.close();
                    }
                }
                catch (Exception e) {
                    logger.warn("Close connection failed after getDatabaseType for DataSource " + dataSource, e);
                }
            }
        }
    }

    public EntityManagerFactoryBuilder getEntityManagerFactoryBuilder() {
        return emFactoryBuilder;
    }

    public EntityManager createEntityManager() {
        checkClosed();
        return createEntityManager(Collections.emptyMap());
    }

    public EntityManager createEntityManager(final Map map) {
        checkClosed();
        //ignore map
        return new EntityManagerImpl(this);
    }

    public boolean isOpen() {
        return (this.dataSource != null) && open;
    }

    public void reOpen(){
        initDataSource();
        open = true;
    }

    public void close() {
        // TODO: check if Query is executing before close, if executing, wait is needed
        checkClosed();
        logger.info("Close EntityManagerFactory: " + persistenceUnit);
        // shutdown data source
        open = false;
        
        if(entityTransaction != null && entityTransaction.isActive()) {
            //Do Not transactionManagerFactory.shutdown(),  will close all EntityManagerFactory's transactionManager :-(
//            transactionManagerFactory.shutdown();
            entityTransaction.rollback();
            entityTransaction = null;
        }
        if (dataSource != null) {
            dataSourceFactory.shutdownDataSource(dataSource);
        }
        if (cacheFactory != null) {
            cacheFactory.close();
        }
    }

    protected DataSource getDataSource() {
        return dataSource;
    }


    public String getUnitName() {
        return unitName;
    }

    public String getJTADataSource() {
        return jtaDataSource;
    }

    public NamedSQLTemplate getNamedQuery(String name) {
        return emFactoryBuilder.getNamedSQLTemplate(name);
    }

    public CacheConfig getCacheConfig() {
        return cacheFactory.getCacheConfig();
    }

    public CacheFactory getCacheFactory() {
        return cacheFactory;
    }

    public void clearCache() {
        cacheFactory.clear();
    }

    public TransactionManagerFactory getTransactionManagerFactory() {
        return transactionManagerFactory;
    }

    public synchronized EntityTransaction getEntityTransaction() {
        if(entityTransaction ==  null) {
            // one EntityManagerFactory only has one EntityTransaction
            entityTransaction = new EntityTransactionImpl(getTransactionManagerFactory());
        }
        return entityTransaction;
    }

    /**
     * used to get cumstom NamedNativeQeury SQL, so NativeJPA can support multi-database
     */
    public String getDatabaseProductName() {
        return databaseProductName;
    }

    public void checkConnection() throws SQLException {
        Connection conn = dataSource.getConnection();
        conn.close();
    }

    protected void checkClosed(){
        if(!isOpen()) {
            throw new IllegalStateException("EntityManager with unitName=" + getUnitName() + " has been closed.");
        }
    }
}
